# Copyright (C) 2001-2004, 2006, 2009 The Written Word, Inc.
# Copyright (C) 2000-2001 The Written Word, LLC
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# $Id: http.py 694 2010-02-19 14:55:45Z china $

import os
import types

import fchksum
import depot
import mysig
import pycurl

from depot import Depot, ProgressAdaptor, CHECKSUM_FILESIZE
from misclib import urljoin
from progress_meter import TextProgressMeter

class http (Depot):
  """Remote package `Depot` over HTTP."""

  def __init__ (self, var, local_depot, dep, sub_depot):
    """Return an HTTP `Depot` object."""

    # This is better, but doesn't work for classic classes:
    #    super (http, self).__init__ (dep)
    # So, we're forced to hardcode the superclass for now:
    Depot.__init__ (self, dep)

    if type (dep) == types.InstanceType:
      self.var = dep.var
    else:
      self.var = var

    # full URL path where packages are to be found
    pkg_path = None
    if type (dep) == types.InstanceType:
      self.pkg_url = urljoin (dep.pkg_url, sub_depot)
    elif var is not None:
      # path to "root" of package directory
      pkg_path = var['dist']

      if pkg_path:
        self.pkg_url = urljoin (self.pkg_url, pkg_path)

    if not local_depot:
      return

    # make local depot mirror the directory structure of the
    # remote depot. This is to make it possible to specify the
    # path of the local depot as a depot to future sessions.
    if type (dep) == types.InstanceType:
      self.local_path = os.path.join (dep.local_path, sub_depot)
    else:
      if pkg_path:
        self.local_path = os.path.join (local_depot, pkg_path)
      else:
        self.local_path = local_depot

    if not os.path.exists (self.local_path):
      os.makedirs (self.local_path)

  def _perform (self, options):
    """An option-setting  wrapper for `pycurl`.

    :Parameters:
        `options`: dictionary
            A dictionary of PYCURL option names and settings. 

    :Exceptions:
         - `DepotFileNotFoundError`: If a remote file is not found.
         - `DepotAccessDeniedError`: If remote authentication failed.
         - `DepotAccessError`: Any other problem fetching.

    Instantiate pycurl and use it to perform an action based on the
    passed `options`.
    """
    c = pycurl.Curl ()

    #c.setopt (pycurl.VERBOSE, 1)

    for (k, v) in options.items ():
      c.setopt (k, v)

    self._proxy_auth (c)
    self._protocol_auth (c)

    try:
      c.perform ()
    except pycurl.error, msg:
      status = msg[0]
      if status == pycurl.E_HTTP_RETURNED_ERROR and msg[1].endswith (' 404'):
	# 404 status from HTTP indicates file not found
        raise depot.DepotFileNotFoundError (self.url)
      elif status == pycurl.E_LOGIN_DENIED:
        raise depot.DepotAccessDeniedError
      else:
        raise depot.DepotAccessError (msg[1])

  def _protocol_auth (self, c):
    """Handle HTTP specific authentication."""
    if self.auth.login:
      c.setopt (pycurl.HTTPAUTH, pycurl.HTTPAUTH_ANY)
      c.setopt (pycurl.USERPWD, self.auth.login + ':' + \
                (self.auth.passwd or ''))

  def _proxy_auth (self, c):
    """Handle proxy configuration."""
    if self.proxy.host:
      c.setopt (pycurl.PROXY, self.proxy.host)
      c.setopt (pycurl.PROXYTYPE, pycurl.PROXYTYPE_HTTP)
      c.setopt (pycurl.PROXYAUTH, pycurl.HTTPAUTH_ANY)

    if self.proxy.login:
      c.setopt (pycurl.PROXYUSERPWD, self.proxy.login + ':' + \
                (self.proxy.passwd or ''))

  def exists (self, filename):
    """Test whether `filename` is present at the remote host.

    :Parameters:
        `filename`: string
            Path to remote file relative to remote repository.

    :Return:
        - True unless remote file cannot be found.
    """

    # save full url in case of error
    _url = urljoin (self.pkg_url, filename)

    try:
      self._perform ({
        pycurl.URL: _url,
        pycurl.NOBODY: 1,
        pycurl.NOPROGRESS: 1,
        pycurl.FAILONERROR: 1,
      })
    except depot.DepotFileNotFoundError:
      return False

    return True

  def fetch (self, filename, progressmeter = None, fetched_bytes = 0,
             additional_bytes = 0):
    """Fetch a local copy of `filename` from the remote machine.

    :Parameters:
        `filename`: string
            Path to remote file relative to remote repository, also
            used for the name of the local copy.
        `progressmeter`: TextProgressMeter
            Re-use `progressmeter` for fetching related files.
        `fetched_bytes`: integer
            Number of bytes already fetched as previous files using
            this `progressmeter`.
        `additional_bytes`: integer
            Additional bytes yet to be downloaded with further files
            using this `progressmeter`.

    :Return:
        - Running total number of bytes downloaded so far in this file
          group.
    """

    # save full url in case of error
    _url = urljoin (self.pkg_url, filename)

    options = {
      pycurl.URL: _url,
      pycurl.WRITEDATA: open (os.path.join (self.local_path, filename), 'wb'),
      pycurl.NOPROGRESS: 1,
    }

    if progressmeter:
      # instantiate a new ProgressAdaptor for every file in the group
      adaptor = ProgressAdaptor (progressmeter, fetched_bytes, 
                                 additional_bytes)
      options[pycurl.PROGRESSFUNCTION] = adaptor.progress_cb
      options[pycurl.NOPROGRESS] = 0

    self._perform (options)

    if progressmeter is not None:
      fetched_bytes = adaptor.get_running_total ()

    return fetched_bytes

  def get (self, pkginst, show_progress = False):
    """Copy `pkginst` file from remote depot to local depot.

    :Parameters:
        `pkginst`: string
            Path to remote pkginst file relative to remote repository.
        `show_progress`: boolean
            True if a `TextProgressMeter` should be used as `pkginst`
            is downloaded.

    :Attributes:
        `verify_checksum`: boolean
            If True, then a checksum file is also fetched as part of
            the same file group (sharing the same `progressmeter`).
    """

    fetched_bytes = 0

    if show_progress:
      progressmeter = TextProgressMeter (self.indent_progressmeter)
    else:
      progressmeter = None

    # fetch a copy of the remote file
    mysig.register (os.remove, os.path.join (self.local_path, pkginst))
    try:
      fetched_bytes = self.fetch (pkginst, progressmeter, 0,
                                  CHECKSUM_FILESIZE)
    except depot.DepotError, msg:
      mysig.unregister (os.remove)
      raise

    if self.verify_checksum:
      # fetch the associated checksum file
      pkginst += '.md5'
      mysig.register (os.remove, os.path.join (self.local_path, pkginst))
      try:
        self.fetch (pkginst, progressmeter, fetched_bytes, 0)
      except depot.DepotError:
        mysig.unregister (os.remove)
        mysig.unregister (os.remove)
        raise

    mysig.unregister (os.remove)
    mysig.unregister (os.remove)

    if show_progress:
      progressmeter.end ()

  def is_remote (self):
    """Return True if a depot with this transport is on a remote server."""
    return True

  def local_uptodate (self, file):
    """Check if file in local depot equivalent to that in remote depot."""
    if not self.verify_checksum:
      raise depot.DepotChecksumsOff (file)

    local_file = os.path.join (self.local_path, file)

    # if file in local depot matches checksum of version in
    # remote repository, do not download
    if os.path.exists (local_file):
      try:
        self.fetch (file + '.md5')
      except depot.DepotFileNotFoundError:
        return False

      md5_checksum = fchksum.fmd5t (local_file)[0]
      fh = open (local_file + '.md5')
      sum = fh.readline ().rstrip ()
      fh.close ()

      if str (sum) == str (md5_checksum):
        return True

    return False
